/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.jca.cci.connection;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.lang.Nullable;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.support.AbstractPlatformTransactionManager;
import org.springframework.transaction.support.DefaultTransactionStatus;
import org.springframework.transaction.support.ResourceTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;

import javax.resource.NotSupportedException;
import javax.resource.ResourceException;
import javax.resource.cci.Connection;
import javax.resource.cci.ConnectionFactory;
import javax.resource.spi.LocalTransactionException;

/**
 * {@link org.springframework.transaction.PlatformTransactionManager} implementation
 * that manages local transactions for a single CCI ConnectionFactory.
 * Binds a CCI Connection from the specified ConnectionFactory to the thread,
 * potentially allowing for one thread-bound Connection per ConnectionFactory.
 *
 * <p>Application code is required to retrieve the CCI Connection via
 * {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)} instead of a standard
 * Java EE-style {@link ConnectionFactory#getConnection()} call. Spring classes such as
 * {@link org.springframework.jca.cci.core.CciTemplate} use this strategy implicitly.
 * If not used in combination with this transaction manager, the
 * {@link ConnectionFactoryUtils} lookup strategy behaves exactly like the native
 * DataSource lookup; it can thus be used in a portable fashion.
 *
 * <p>Alternatively, you can allow application code to work with the standard
 * Java EE lookup pattern {@link ConnectionFactory#getConnection()}, for example
 * for legacy code that is not aware of Spring at all. In that case, define a
 * {@link TransactionAwareConnectionFactoryProxy} for your target ConnectionFactory,
 * which will automatically participate in Spring-managed transactions.
 *
 * @author Thierry Templier
 * @author Juergen Hoeller
 * @see ConnectionFactoryUtils#getConnection(javax.resource.cci.ConnectionFactory)
 * @see ConnectionFactoryUtils#releaseConnection
 * @see TransactionAwareConnectionFactoryProxy
 * @see org.springframework.jca.cci.core.CciTemplate
 * @since 1.2
 */
@SuppressWarnings("serial")
public class CciLocalTransactionManager extends AbstractPlatformTransactionManager
        implements ResourceTransactionManager, InitializingBean {

    @Nullable
    private ConnectionFactory connectionFactory;


    /**
     * Create a new CciLocalTransactionManager instance.
     * A ConnectionFactory has to be set to be able to use it.
     *
     * @see #setConnectionFactory
     */
    public CciLocalTransactionManager() {
    }

    /**
     * Create a new CciLocalTransactionManager instance.
     *
     * @param connectionFactory the CCI ConnectionFactory to manage local transactions for
     */
    public CciLocalTransactionManager(ConnectionFactory connectionFactory) {
        setConnectionFactory(connectionFactory);
        afterPropertiesSet();
    }

    /**
     * Return the CCI ConnectionFactory that this instance manages local
     * transactions for.
     */
    @Nullable
    public ConnectionFactory getConnectionFactory() {
        return this.connectionFactory;
    }

    /**
     * Set the CCI ConnectionFactory that this instance should manage local
     * transactions for.
     */
    public void setConnectionFactory(@Nullable ConnectionFactory cf) {
        if (cf instanceof TransactionAwareConnectionFactoryProxy) {
            // If we got a TransactionAwareConnectionFactoryProxy, we need to perform transactions
            // for its underlying target ConnectionFactory, else JMS access code won't see
            // properly exposed transactions (i.e. transactions for the target ConnectionFactory).
            this.connectionFactory = ((TransactionAwareConnectionFactoryProxy) cf).getTargetConnectionFactory();
        }
        else {
            this.connectionFactory = cf;
        }
    }

    private ConnectionFactory obtainConnectionFactory() {
        ConnectionFactory connectionFactory = getConnectionFactory();
        Assert.state(connectionFactory != null, "No ConnectionFactory set");
        return connectionFactory;
    }

    @Override
    public void afterPropertiesSet() {
        if (getConnectionFactory() == null) {
            throw new IllegalArgumentException("Property 'connectionFactory' is required");
        }
    }


    @Override
    public Object getResourceFactory() {
        return obtainConnectionFactory();
    }

    @Override
    protected Object doGetTransaction() {
        CciLocalTransactionObject txObject = new CciLocalTransactionObject();
        ConnectionHolder conHolder =
                (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainConnectionFactory());
        txObject.setConnectionHolder(conHolder);
        return txObject;
    }

    @Override
    protected boolean isExistingTransaction(Object transaction) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) transaction;
        // Consider a pre-bound connection as transaction.
        return txObject.hasConnectionHolder();
    }

    @Override
    protected void doBegin(Object transaction, TransactionDefinition definition) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) transaction;
        ConnectionFactory connectionFactory = obtainConnectionFactory();
        Connection con = null;

        try {
            con = connectionFactory.getConnection();
            if (logger.isDebugEnabled()) {
                logger.debug("Acquired Connection [" + con + "] for local CCI transaction");
            }

            ConnectionHolder connectionHolder = new ConnectionHolder(con);
            connectionHolder.setSynchronizedWithTransaction(true);

            con.getLocalTransaction().begin();
            int timeout = determineTimeout(definition);
            if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
                connectionHolder.setTimeoutInSeconds(timeout);
            }

            txObject.setConnectionHolder(connectionHolder);
            TransactionSynchronizationManager.bindResource(connectionFactory, connectionHolder);
        }
        catch (NotSupportedException ex) {
            ConnectionFactoryUtils.releaseConnection(con, connectionFactory);
            throw new CannotCreateTransactionException("CCI Connection does not support local transactions", ex);
        }
        catch (LocalTransactionException ex) {
            ConnectionFactoryUtils.releaseConnection(con, connectionFactory);
            throw new CannotCreateTransactionException("Could not begin local CCI transaction", ex);
        }
        catch (Throwable ex) {
            ConnectionFactoryUtils.releaseConnection(con, connectionFactory);
            throw new TransactionSystemException("Unexpected failure on begin of CCI local transaction", ex);
        }
    }

    @Override
    protected Object doSuspend(Object transaction) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) transaction;
        txObject.setConnectionHolder(null);
        return TransactionSynchronizationManager.unbindResource(obtainConnectionFactory());
    }

    @Override
    protected void doResume(@Nullable Object transaction, Object suspendedResources) {
        ConnectionHolder conHolder = (ConnectionHolder) suspendedResources;
        TransactionSynchronizationManager.bindResource(obtainConnectionFactory(), conHolder);
    }

    protected boolean isRollbackOnly(Object transaction) throws TransactionException {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) transaction;
        return txObject.getConnectionHolder().isRollbackOnly();
    }

    @Override
    protected void doCommit(DefaultTransactionStatus status) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Committing CCI local transaction on Connection [" + con + "]");
        }
        try {
            con.getLocalTransaction().commit();
        }
        catch (LocalTransactionException ex) {
            throw new TransactionSystemException("Could not commit CCI local transaction", ex);
        }
        catch (ResourceException ex) {
            throw new TransactionSystemException("Unexpected failure on commit of CCI local transaction", ex);
        }
    }

    @Override
    protected void doRollback(DefaultTransactionStatus status) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) status.getTransaction();
        Connection con = txObject.getConnectionHolder().getConnection();
        if (status.isDebug()) {
            logger.debug("Rolling back CCI local transaction on Connection [" + con + "]");
        }
        try {
            con.getLocalTransaction().rollback();
        }
        catch (LocalTransactionException ex) {
            throw new TransactionSystemException("Could not roll back CCI local transaction", ex);
        }
        catch (ResourceException ex) {
            throw new TransactionSystemException("Unexpected failure on rollback of CCI local transaction", ex);
        }
    }

    @Override
    protected void doSetRollbackOnly(DefaultTransactionStatus status) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) status.getTransaction();
        if (status.isDebug()) {
            logger.debug("Setting CCI local transaction [" + txObject.getConnectionHolder().getConnection() +
                    "] rollback-only");
        }
        txObject.getConnectionHolder().setRollbackOnly();
    }

    @Override
    protected void doCleanupAfterCompletion(Object transaction) {
        CciLocalTransactionObject txObject = (CciLocalTransactionObject) transaction;
        ConnectionFactory connectionFactory = obtainConnectionFactory();

        // Remove the connection holder from the thread.
        TransactionSynchronizationManager.unbindResource(connectionFactory);
        txObject.getConnectionHolder().clear();

        Connection con = txObject.getConnectionHolder().getConnection();
        if (logger.isDebugEnabled()) {
            logger.debug("Releasing CCI Connection [" + con + "] after transaction");
        }
        ConnectionFactoryUtils.releaseConnection(con, connectionFactory);
    }


    /**
     * CCI local transaction object, representing a ConnectionHolder.
     * Used as transaction object by CciLocalTransactionManager.
     *
     * @see ConnectionHolder
     */
    private static class CciLocalTransactionObject {

        @Nullable
        private ConnectionHolder connectionHolder;

        public ConnectionHolder getConnectionHolder() {
            Assert.state(this.connectionHolder != null, "No ConnectionHolder available");
            return this.connectionHolder;
        }

        public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) {
            this.connectionHolder = connectionHolder;
        }

        public boolean hasConnectionHolder() {
            return (this.connectionHolder != null);
        }
    }

}
